import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  HostBinding,
  Inject,
  Input,
  OnDestroy,
  OnInit,
  SkipSelf,
  inject,
} from '@angular/core';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import {
  AppProfileService,
  PORTMASTER_HTTP_API_ENDPOINT,
  PortapiService,
  Record,
  deepClone,
} from '@safing/portmaster-api';
import { Subscription, map, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { AppIconResolver } from './app-icon-resolver';
import { AppProfile } from 'projects/safing/portmaster-api/src/public-api';

// Interface that must be satisfied for the profile-input
// of app-icon.
export interface IDandName {
  // ID of the profile.
  ID?: string;

  // Source is the source of the profile.
  Source?: string;

  // Name of the profile.
  Name: string;
}

// Some icons we don't want to show on the UI.
// Note that this works on a best effort basis and might
// start breaking with updates to the built-in icons...
const iconBlobsToIgnore = [
  '',
  '',
  '',
  '',
  '',
  '',
];
const iconIDsToIgnore = [
  "a27898ddfa4e0481b62c69faa196919a738fcade",
	"5a3eea8bcd08b9336ce9c5083f26185164268ee9",
	"573393d6ad238d255b20dc1c1b303c95debe6965",
	"d459b2cb23c27cc31ccab5025533048d5d8301bf",
	"d35a0d91ebfda81df5286f68ec5ddb1d6ad6b850",
	"cc33187385498384f1b648e23be5ef1a2e9f5f71",
];


const profilesToIgnore = ['local/_unidentified', 'local/_unsolicited'];

@Component({
  selector: 'app-icon',
  templateUrl: './app-icon.html',
  styleUrls: ['./app-icon.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppIconComponent implements OnInit, OnDestroy {
  private sub = Subscription.EMPTY;
  private initDone = false;

  private resovler = inject(AppIconResolver);

  /** @private The data-URL for the app-icon if available */
  src: SafeUrl | string = '';

  /** The profile for which to show the app-icon */
  @Input()
  set profile(p: IDandName | null | undefined | string) {
    if (typeof p === 'string') {
      const parts = p.split("/")
      p = {
        Source: parts[0],
        ID: parts[1],
        Name: '',
      }
    }

    if (!!this._profile && !!p && this._profile.ID === p.ID) {
      // skip if this is the same profile
      return;
    }

    this._profile = p || null;

    if (this.initDone) {
      this.updateView();
    }
  }
  get profile(): IDandName | null | undefined {
    return this._profile;
  }
  private _profile: IDandName | null = null;

  /** isIgnoredProfile is set to true if the profile is part of profilesToIgnore */
  isIgnoredProfile = false;

  /** If not icon is available, this holds the first - uppercased - letter of the app - name */
  letter = '';

  /** @private The background color of the component, based on icon availability and generated by ID */
  @HostBinding('style.background-color')
  color = 'var(--text-tertiary)';

  constructor(
    private profileService: AppProfileService,
    private changeDetectorRef: ChangeDetectorRef,
    private portapi: PortapiService,
    // @HostBinding() is not evaluated in our change-detection run but rather
    // checked by the parent component during updateRenderer.
    // Since we want the background color to change immediately after we set the
    // src path we need to tell the parent (which ever it is) to update as wel.
    @SkipSelf() private parentCdr: ChangeDetectorRef,
    private sanitzier: DomSanitizer,
    @Inject(PORTMASTER_HTTP_API_ENDPOINT) private httpAPI: string
  ) { }

  /** Updates the view of the app-icon and tries to find the actual application icon */
  private requestedAnimationFrame: number | null = null;
  private updateView(skipIcon = false) {
    if (this.requestedAnimationFrame !== null) {
      cancelAnimationFrame(this.requestedAnimationFrame);
    }

    this.requestedAnimationFrame = requestAnimationFrame(() => {
      this.__updateView(skipIcon);
    })
  }

  ngOnInit(): void {
    this.updateView();
    this.initDone = true;
  }

  private __updateView(skipIcon = false) {
    this.requestedAnimationFrame = null;

    const p = this.profile;
    const sourceAndId = this.getIDAndSource();

    if (!!p && sourceAndId !== null) {
      let idx = 0;
      for (let i = 0; i < (p.ID || p.Name).length; i++) {
        idx += (p.ID || p.Name).charCodeAt(i);
      }

      const combinedID = `${sourceAndId[0]}/${sourceAndId[1]}`;
      this.isIgnoredProfile = profilesToIgnore.includes(combinedID);

      this.updateLetter(p);

      if (!this.isIgnoredProfile) {
        this.color = AppColors[idx % AppColors.length];
      } else {
        this.color = 'transparent';
      }

      if (!skipIcon) {
        this.tryGetSystemIcon();
      }

    } else {
      this.isIgnoredProfile = false;
      this.color = 'var(--text-tertiary)';
    }

    this.changeDetectorRef.markForCheck();
    this.parentCdr.markForCheck();
  }

  private updateLetter(p: IDandName) {
    if (p.Name !== '') {
      if (p.Name[0] === '<') {
        // we might get the name with search-highlighting which
        // will then include <em> tags. If the first character is a <
        // make sure to strip all HTML tags before getting [0].
        this.letter = p.Name.replace(
          /(&nbsp;|<([^>]+)>)/gi,
          ''
        )[0].toLocaleUpperCase();
      } else {
        this.letter = p.Name[0];
      }

      this.letter = this.letter.toLocaleUpperCase();
    } else {
      this.letter = '?';
    }
  }

  getIDAndSource(): [string, string] | null {
    if (!this.profile) {
      return null;
    }

    const id = this.profile.ID;
    if (!id) {
      return null;
    }

    // if there's a source ID only holds the profile ID
    if (!!this.profile.Source) {
      return [this.profile.Source, id];
    }

    // otherwise, ID likely contains the source
    const [source, ...rest] = id.split('/');
    if (rest.length > 0) {
      return [source, rest.join('/')];
    }

    // id does not contain a forward-slash so we
    // assume the source is local
    return ['local', id];
  }

  /**
   * Tries to get the application icon form the system.
   * Requires the app to be running in the electron wrapper.
   */
  private tryGetSystemIcon() {
    const sourceAndId = this.getIDAndSource();
    if (sourceAndId === null) {
      return;
    }

    this.sub.unsubscribe();

    this.sub = this.profileService
      .watchAppProfile(sourceAndId[0], sourceAndId[1])
      .pipe(
        switchMap((profile: AppProfile) => {
          this.updateLetter(profile);

          if (!!profile.Icons?.length) {
            const firstIcon = profile.Icons[0];

            console.log(`profile ${profile.Name} has icon of from source ${firstIcon.Source} stored in ${firstIcon.Type}`)

            switch (firstIcon.Type) {
              case 'database':
                return this.portapi
                  .get<Record & { iconData: string }>(firstIcon.Value)
                  .pipe(
                    map((result) => {
                      return result.iconData;
                    })
                  );

              case 'api':
                return of(`${this.httpAPI}/v1/profile/icon/${firstIcon.Value}`);

              case 'path':
                // TODO: Silently ignore for now.
                return of('');

              case '':
                // Icon is not set.
                return of('');

              default:
                console.error(`Icon type ${firstIcon.Type} not yet supported`);
            }
          }

          this.resovler.resolveIcon(profile);

          // return an empty icon here. If the resolver manages to find an icon
          // the profle will get updated and we'll run again here.
          return of('');
        })
      )
      .subscribe({
        next: (icon) => {
          if (iconBlobsToIgnore.some((i) => i === icon)) {
            icon = '';
          } else if (iconIDsToIgnore.some((i) => icon.includes(i))) {
            // TODO: This just checks if the value (blob, URL, etc.) contains
            // the SHA1 sum of the icon, which is used in the URL of api icon types.
            // This is very unlikely to have false positivies, but this could still
            // be done a lot cleaner.
            icon = '';
          }
          if (icon !== '') {
            this.src = this.sanitzier.bypassSecurityTrustUrl(icon);
            this.color = 'unset';
          } else {
            this.src = '';
            this.color =
              this.color === 'unset' ? 'var(--text-tertiary)' : this.color;
          }
          this.changeDetectorRef.detectChanges();
          this.parentCdr.markForCheck();
        },
        error: (err) => console.error(err),
      });
  }

  ngOnDestroy(): void {
    this.sub.unsubscribe();
  }
}

export const AppColors: string[] = [
  'rgba(244, 67, 54, .7)',
  'rgba(233, 30, 99, .7)',
  'rgba(156, 39, 176, .7)',
  'rgba(103, 58, 183, .7)',
  'rgba(63, 81, 181, .7)',
  'rgba(33, 150, 243, .7)',
  'rgba(3, 169, 244, .7)',
  'rgba(0, 188, 212, .7)',
  'rgba(0, 150, 136, .7)',
  'rgba(76, 175, 80, .7)',
  'rgba(139, 195, 74, .7)',
  'rgba(205, 220, 57, .7)',
  'rgba(255, 235, 59, .7)',
  'rgba(255, 193, 7, .7)',
  'rgba(255, 152, 0, .7)',
  'rgba(255, 87, 34, .7)',
  'rgba(121, 85, 72, .7)',
  'rgba(158, 158, 158, .7)',
  'rgba(96, 125, 139, .7)',
];
